<?php

/**
 * CCoreNetImap
 * CCoreNetImapResponse
 * CCoreNetImapFolder
 * CCoreNetImapConfig
 *
 * @author Usenko Timur <usenko.timur@gmail.com>
 * @package net
 * @subpackage imap
 */

CCore::Using('base.utils');
CCore::Using('net.abstract');
CCore::Using('net.config');
CCore::Using('net.imap.enum');
CCore::Using('net.imap.stream');

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImap extends ACoreNetAbstract
{
	/**
	 * @var string
	 */
	const TAG_PREFIX = 'TAG';

	/**
	 * @var string
	 */
	private $_sResponseBuffer;

	/**
	 * @var int
	 */
	private $_iResponseBufParsedPos;

	/**
	 * @var int
	 */
	private $_iTagCount;

	/**
	 * @var array
	 */
	private $_aCapabilityItems;

	/**
	 * @var CCoreNetImapFolderInformation
	 */
	private $_oCurrentFolderInfo;

	/**
	 * @var array
	 */
	private $_aLastResponse;

	/**
	 * @var array
	 */
	private $_aFetchCallbacks;

	/**
	 * @param CCoreNetImapConfig $oImapConfig
	 * @return CCoreNetImap
	 */
	public function __construct(CCoreNetImapConfig $oImapConfig)
	{
		CCore::CheckTypes(array('$oImapConfig'));

		parent::__construct($oImapConfig);

		$this->oFollowers->InitPrefixs('IMAP', 'IMAP[Error]', 'IMAP[Warning]', 'IMAP[Notice]');
		$this->oFollowers->SetSecretValues(array(
			$oImapConfig->GetValue('Host'), $oImapConfig->GetValue('Login'), $oImapConfig->GetValue('Password')
		));

		$this->_iTagCount = 0;
		$this->_aCapabilityItems = null;
		$this->_oCurrentFolderInfo = null;
		$this->_aFetchCallbacks = null;
		$this->_sResponseBuffer = '';
		$this->_iResponseBufParsedPos = 0;
		$this->_aLastResponse = array();
	}

	/**
	 * @return bool
	 */
	public function Connect()
	{
		$iPort = $this->oConfig->GetValue('Port', 'int');

		if (parent::Connect(993 === $iPort))
		{
			$aResult = $this->ParseResponse('*');
			if ($this->_isSuccess($aResult))
			{
				$this->_parseConnectCapability($aResult);
				return true;
			}
		}
		return false;
	}

	/**
	 * @return bool
	 */
	public function Login()
	{
		$sLogin = $this->oConfig->GetValue('Login', 'string');
		$sPassword = $this->oConfig->GetValue('Password', 'string');

		return $this->SendRequestWithCheck('LOGIN',
			array($this->_escapeString($sLogin, true), $this->_escapeString($sPassword, true)));
	}

	/**
	 * @return bool
	 */
	public function ConnectAndLogin()
	{
		return $this->Connect() && $this->Login();
	}

	/**
	 * @return bool
	 */
	public function Logout()
	{
		return $this->SendRequestWithCheck('LOGOUT');
	}

	/**
	 * @return bool
	 */
	public function Capability()
	{
		$aCapability = array();
		$sTag = $this->_getNewTag();
		if ($this->SendRequest($sTag, 'CAPABILITY'))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				foreach ($aResult as /* @var $oImapRespon CCoreNetImapResponse */ $oImapRespon)
				{
					if (ECoreNetImapResponseType::Untagged === $oImapRespon->ResponseType &&
						2 < count($oImapRespon->ResponseList) && 'CAPABILITY' === $oImapRespon->ResponseList[1])
					{
						for ($iI = 2, $iC = count($oImapRespon->ResponseList); $iI < $iC; $iI++)
						{
							$aCapability[] = strtoupper($oImapRespon->ResponseList[$iI]);
						}
						break;
					}
				}
			}
		}
		return $aCapability;
	}

	/**
	 * @param array $aResult
	 * @return void
	 */
	private function _parseConnectCapability($aResult)
	{
		$aCapability = null;
		if (isset($aResult[0]) && is_array($aResult[0]->OptionalResponse)
			&& count($aResult[0]->OptionalResponse) && 0 < count($aResult[0]->OptionalResponse)
			&& 'CAPABILITY' === $aResult[0]->OptionalResponse[0])
		{
			for ($iI = 1, $iC = count($aResult[0]->OptionalResponse); $iI < $iC; $iI++)
			{
				$aCapability[] = strtoupper($aResult[0]->OptionalResponse[$iI]);
			}
		}
		
		$this->_aCapabilityItems = $aCapability;
	}

	/**
	 * @param string $sExtentionName
	 * @return bool
	 */
	public function IsSupport($sExtentionName)
	{
		CCore::CheckTypes(array('$sExtentionName'));

		if (null === $this->_aCapabilityItems)
		{
			$this->_aCapabilityItems = $this->Capability();
		}

		if (is_array($this->_aCapabilityItems))
		{
			return in_array(strtoupper($sExtentionName), $this->_aCapabilityItems);
		}

		return false;
	}

	/**
	 * @return bool
	 */
	public function Noop()
	{
		return $this->SendRequestWithCheck('NOOP');
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderCreate($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->SendRequestWithCheck('CREATE',
			array($this->_escapeString($sFolderName, true)));
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderDelete($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->SendRequestWithCheck('DELETE',
			array($this->_escapeString($sFolderName, true)));
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderSubscribe($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->SendRequestWithCheck('SUBSCRIBE',
			array($this->_escapeString($sFolderName, true)));
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderUnSubscribe($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->SendRequestWithCheck('UNSUBSCRIBE',
			array($this->_escapeString($sFolderName, true)));
	}

	/**
	 * @param string $sOldFolderName
	 * @param string $sNewFolderName
	 * @return bool
	 */
	public function FolderRename($sOldFolderName, $sNewFolderName)
	{
		CCore::CheckTypes(array('$sOldFolderName', '$sNewFolderName'));

		return $this->SendRequestWithCheck('RENAME', array(
			$this->_escapeString($sOldFolderName, true),
			$this->_escapeString($sNewFolderName, true)));
	}

	/**
	 * @param string $sFolderName
	 * @param array $aStatusItems
	 * @return array | false
	 */
	public function FolderStatus($sFolderName, array $aStatusItems)
	{
		if (count($aStatusItems) > 0)
		{
			$sTag = $this->_getNewTag();
			if ($this->SendRequest($sTag, 'STATUS',
				array($this->_escapeString($sFolderName, true), $aStatusItems)))
			{
				$aResult = $this->ParseResponse($sTag);
				if ($this->_isSuccess($aResult))
				{
					return $this->_getStatusFolderInformation($aResult);
				}
			}
		}

		return false;
	}

	/**
	 * @return array | false
	 */
	public function FolderList($sParentFolderName = '', $sListPattern = '*')
	{
		return $this->_folderList(false, $sParentFolderName, $sListPattern);
	}

	/**
	 * @return array | false
	 */
	public function FolderSubscribeList($sParentFolderName = '', $sListPattern = '*')
	{
		return $this->_folderList(true, $sParentFolderName, $sListPattern);
	}

	/**
	 * @return array | false
	 */
	public function _folderList($bIsSubscribeList, $sParentFolderName = '', $sListPattern = '*')
	{
		$sTag = $this->_getNewTag();
		$sCmd = 'LSUB';
		if (!$bIsSubscribeList)
		{
			$sCmd = ($this->IsSupport('XLIST')) ? 'XLIST' : 'LIST';
		}
		
		if ($this->SendRequest($sTag, $sCmd,
			array($this->_escapeString($sParentFolderName, true), $this->_escapeString($sListPattern, true))))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				return $this->_getFoldersFromResult($aResult, $sCmd);
			}
		}

		return false;
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderSelect($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->_selectOrExamineFolder($sFolderName, true);
	}

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	public function FolderExamine($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->_selectOrExamineFolder($sFolderName, false);
	}

	/**
	 * @param array $aInputFetchItems
	 * @param string $sIndexRange
	 * @param bool $bIndexIsUid
	 * @return array | false
	 */
	public function Fetch(array $aInputFetchItems, $sIndexRange, $bIndexIsUid)
	{
		CCore::CheckTypes(array('$aInputFetchItems', '$sIndexRange', '$bIndexIsUid'));

		$sIndexRange = (string) $sIndexRange;
		if (0 === strlen(trim($sIndexRange)))
		{
			return false;
		}
		
		$aFetchItems = ECoreNetImapFetch::ChangeFetchItemsBefourRequest($aInputFetchItems);
		$aRequest = array($sIndexRange, array_keys($aFetchItems));
		
		$sTag = $this->_getNewTag();
		$sCommandPrefix = ($bIndexIsUid) ? 'UID ' : '';
		if ($this->SendRequest($sTag, $sCommandPrefix.'FETCH', $aRequest))
		{
			$aResult = $this->ParseResponse($sTag, $aFetchItems);
			if ($this->_isSuccess($aResult))
			{
				$aReturn = array();
				foreach ($aResult as &$oImapResponse)
				{
					if (CCoreNetImapFetchResponse::IsValidFetchImapResponse($oImapResponse))
					{
						$aReturn[] = new CCoreNetImapFetchResponse($oImapResponse);
					}
				}
				return $aReturn;
			}
		}
		return false;
	}
	
	/**
	 * @param string $sSearchCriterias
	 * @param bool $bReturnUid
	 * @param string $nsCharset = null
	 * @param string $nsSort = null
	 * @return array | false
	 */
	public function MessageSimpleSearch($sSearchCriterias, $bReturnUid, $nsCharset = null, $nsSort = null)
	{
		CCore::CheckTypes(array('$sSearchCriterias', '$bReturnUid', '$nsCharset'));

		$sTag = $this->_getNewTag();
		$sCommandPrefix = ($bReturnUid) ? 'UID ' : '';

		if (null !== $nsSort && !$this->IsSupport('SORT'))
		{
			$nsSort = null;
		}

		$aRequest = array();
		if (null !== $nsSort)
		{
			$aRequest[] = array($nsSort);
		}
		
		if (null !== $nsCharset)
		{
			if (null === $nsSort)
			{
				$aRequest[] = 'CHARSET';
			}
			$aRequest[] = strtoupper($nsCharset);
		}
		else if (null !== $nsSort)
		{
			$aRequest[] = 'UTF-8';
		}
		
		$aRequest[] = $sSearchCriterias;

		$sCmd = (null !== $nsSort) ? 'SORT' : 'SEARCH';
		
		if ($this->SendRequest($sTag, $sCommandPrefix.$sCmd, $aRequest))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				$aReturn = array();
				foreach ($aResult as /* @var $oImapRespons CCoreNetImapResponse */ $oImapRespons)
				{
					if (ECoreNetImapResponseType::Untagged === $oImapRespons->ResponseType
						&& $sCmd === $oImapRespons->StatusOrIndex
						&& is_array($oImapRespons->ResponseList) && 2 < count($oImapRespons->ResponseList))
					{
						for ($iI = 2, $iC = count($oImapRespons->ResponseList); $iI < $iC; $iI++)
						{
							$aReturn[] = (int) $oImapRespons->ResponseList[$iI];
						}
					}
				}
				return $aReturn;
			}
		}
		return false;
	}

	/**
	 * @param string $sIndexRange
	 * @param bool $bIndexIsUid
	 * @param array $aInputStoreItems
	 * @param string $sStoreAction
	 * @return bool
	 */
	public function MessageExpunge($sIndexRange, $bIndexIsUid)
	{
		CCore::CheckTypes(array('$sIndexRange', '$bIndexIsUid'));
		if (empty($sIndexRange))
		{
			return false;
		}

		$sTag = $this->_getNewTag();
		$sCommandPrefix = ($bIndexIsUid && ($this->IsSupport('UIDPLUS'))) ? 'UID ' : '';
		if ($this->SendRequest($sTag, $sCommandPrefix.'EXPUNGE', array($sIndexRange)))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * @param string $sIndexRange
	 * @param bool $bIndexIsUid
	 * @param array $aInputStoreItems
	 * @param string $sStoreAction
	 * @return bool
	 */
	public function MessageStoreFlag($sIndexRange, $bIndexIsUid, $aInputStoreItems, $sStoreAction)
	{
		CCore::CheckTypes(array('$sIndexRange', '$bIndexIsUid', '$aInputStoreItems', '$sStoreAction'));
		if (empty($sIndexRange) || empty($sStoreAction) || 0 === count($aInputStoreItems))
		{
			return false;
		}

		$sTag = $this->_getNewTag();
		$sCommandPrefix = ($bIndexIsUid) ? 'UID ' : '';
		if ($this->SendRequest($sTag, $sCommandPrefix.'STORE', array($sIndexRange, $sStoreAction, $aInputStoreItems)))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				return true;
			}
		}
		
		return false;
	}

	/**
	 * @param string $sMessageAppendData
	 * @param string $sFolderName
	 * @param array	$naAppendFlags = null
	 * @return bool
	 */
	public function MessageAppend($sMessageAppendData, $sFolderName, $naAppendFlags = null)
	{
		CCore::CheckTypes(array('$sMessageAppendData', '$sFolderName', '$naAppendFlags'));

		$sTag = $this->_getNewTag();
		if ($this->SendRequest($sTag, 'APPEND',
			array($this->_escapeString($sFolderName, true), $naAppendFlags, '{'.strlen($sMessageAppendData).'}')))
		{
			$aResult = $this->ParseResponse('+');
			if ($this->_isNextSuccess($aResult))
			{
				$this->oFollowers->Write('#INFO: Appented '.strlen($sMessageAppendData).' bytes.',
					CCoreBaseFollowersCollection::INFO);

				fwrite($this->rConnect, $sMessageAppendData);
				fwrite($this->rConnect, C_SOCKET_CRLF);
				return $this->_isSuccess($this->ParseResponse($sTag));
			}
		}
		return false;
	} 

	/**
	 * @param string $sFolderName
	 * @return bool
	 */
	private function _selectOrExamineFolder($sFolderName, $bIsWrite)
	{
		CCore::CheckTypes(array('$sFolderName'));

		$sTag = $this->_getNewTag();
		if ($this->SendRequest($sTag, ($bIsWrite) ? 'SELECT' : 'EXAMINE', array($this->_escapeString($sFolderName, true))))
		{
			$aResult = $this->ParseResponse($sTag);
			if ($this->_isSuccess($aResult))
			{
				$this->_initCurrentFolderInformation($aResult, $sFolderName, $bIsWrite);
				return true;
			}
		}
		return false;
	}

	/**
	 * @param array $aResult
	 * @param bool $bIsSubscribeList
	 * @return array
	 */
	public function _getFoldersFromResult(array $aResult, $sStatus)
	{
		$aReturn = array();
		foreach ($aResult as /* @var $oImapRespons CCoreNetImapResponse */ $oImapRespons)
		{
			if (ECoreNetImapResponseType::Untagged === $oImapRespons->ResponseType &&
				$sStatus === $oImapRespons->StatusOrIndex && 5 === count($oImapRespons->ResponseList))
			{
				$oFolder = new CCoreNetImapFolder();
				if (is_array($oImapRespons->ResponseList[2]))
				{
					$oFolder->Flags = $oImapRespons->ResponseList[2];
					$oFolder->Flags = array_map('strtolower', $oFolder->Flags);
				}
				$oFolder->Delimiter = $oImapRespons->ResponseList[3];
				$oFolder->FullName = (in_array('\inbox', $oFolder->Flags))
					? 'INBOX' : $oImapRespons->ResponseList[4];
				$aReturn[] = $oFolder;
			}
		}
		return $aReturn;
	}
	
	/**
	 * @param array $aResult
	 * @return array
	 */
	public function _getStatusFolderInformation(array $aResult)
	{
		$aReturn = array();
		foreach ($aResult as /* @var $oImapRespons CCoreNetImapResponse */ $oImapRespons)
		{
			if (ECoreNetImapResponseType::Untagged === $oImapRespons->ResponseType &&
				'STATUS' === $oImapRespons->StatusOrIndex && isset($oImapRespons->ResponseList[3]) &&
				is_array($oImapRespons->ResponseList[3]))
			{
				$sName = null;
				foreach ($oImapRespons->ResponseList[3] as $sArrayItem)
				{
					if (null === $sName)
					{
						$sName = $sArrayItem;
						continue;
					}
					else
					{
						$aReturn[$sName] = $sArrayItem;
						$sName = null;
						continue;
					}
				}
			}
		}
		return $aReturn;
	}

	/**
	 * @param array $aResult
	 * @param string $sFolderName
	 * @param bool $bIsWrite
	 */
	public function _initCurrentFolderInformation(array $aResult, $sFolderName, $bIsWrite)
	{
		$oResult = new CCoreNetImapFolderInformation($sFolderName, $bIsWrite);
		foreach ($aResult as /* @var $oImapRespons CCoreNetImapResponse */ $oImapRespons)
		{
			if (ECoreNetImapResponseType::Untagged === $oImapRespons->ResponseType)
			{
				if (count($oImapRespons->ResponseList) > 2 &&
					'FLAGS' === $oImapRespons->ResponseList[1] && is_array($oImapRespons->ResponseList[2]))
				{
					$oResult->Flags = $oImapRespons->ResponseList[2];
				}

				if (is_array($oImapRespons->OptionalResponse) && count($oImapRespons->OptionalResponse) > 1)
				{
					if ('PERMANENTFLAGS' === $oImapRespons->OptionalResponse[0] && is_array($oImapRespons->OptionalResponse[1]))
					{
						$oResult->PermanentFlags = $oImapRespons->OptionalResponse[1];
					}
					else if ('UIDVALIDITY' === $oImapRespons->OptionalResponse[0] && isset($oImapRespons->OptionalResponse[1]))
					{
						$oResult->Uidvalidity = $oImapRespons->OptionalResponse[1];
					}
				}

				if (count($oImapRespons->ResponseList) > 2)
				{
					if ('EXISTS' === $oImapRespons->ResponseList[2])
					{
						$oResult->Exists = (int) $oImapRespons->ResponseList[1];
					}
					else if ('RECENT' === $oImapRespons->ResponseList[2])
					{
						$oResult->Recent = (int) $oImapRespons->ResponseList[1];
					}
				}
			}
		}
		
		$this->_oCurrentFolderInfo = $oResult;
	}

	/**
	 * @return CCoreNetImapFolderInformation
	 */
	public function FolderCurrentInformation()
	{
		return $this->_oCurrentFolderInfo;
	}

	/**
	 * @return bool
	 */
	public function LogoutAndDisconnect()
	{
		return $this->Logout() && $this->Disconnect();
	}

	public function SendRequest($sTag, $sCommand, $naParams = array())
	{
		if (is_resource($this->rConnect))
		{
			$sCommand = $sTag.' '.$sCommand.$this->_prepearParamLine($naParams).C_SOCKET_CRLF;
			$this->oFollowers->Write('> '.CCoreBaseUtils::ShowCRLF($sCommand),
				CCoreBaseFollowersCollection::INFO);
			
			return false !== fwrite($this->rConnect, $sCommand);
		}
		return false;
	}

	public function SendRequestWithCheck($sCommand, $naParams = array())
	{
		$sTag = $this->_getNewTag();
		if ($this->SendRequest($sTag, $sCommand, $naParams))
		{
			return $this->_isSuccess($this->ParseResponse($sTag));
		}
		return false;
	}

	/**
	 * @return array
	 */
	public function GetLastResponse()
	{
		return $this->_aLastResponse;
	}

	public function _getNextBuffer($iReadLen = null)
	{
		if (null === $iReadLen)
		{
			$this->_sResponseBuffer = fgets($this->rConnect);
		}
		else
		{
			$this->_sResponseBuffer = '';
			$iRead = $iReadLen;
			while (0 < $iRead)
			{
				$sAddRead = fread($this->rConnect, $iRead);
				if (false !== $sAddRead)
				{
					$this->_sResponseBuffer .= $sAddRead;
					$iRead -= strlen($sAddRead);
				}
				else
				{
					break;
				}
			}
		}

		if (false === $this->_sResponseBuffer)
		{
			$this->oFollowers->Write('< stream return: false', CCoreBaseFollowersCollection::ERROR);
		}
		else
		{
			if (null === $iReadLen)
			{
				$this->oFollowers->Write('< '.CCoreBaseUtils::ShowCRLF($this->_sResponseBuffer),
					CCoreBaseFollowersCollection::INFO);
			}
			else
			{
				$this->oFollowers->Write('#INFO: Received '.strlen($this->_sResponseBuffer).'/'.$iReadLen.' bytes.',
					CCoreBaseFollowersCollection::INFO);
			}
		}
	}

	public function ParseResponse($sEndTag, array $naFetchItems = null)
	{
		$aResult = array();
		if (is_resource($this->rConnect))
		{
			if (null !== $naFetchItems && 0 < count($naFetchItems))
			{
				foreach ($naFetchItems as /* @var $oFetchItem CCoreNetImapFetchItem */ $oFetchItem)
				{
					if (CCoreNetImapFetchItem::ReturnTypeCallback === $oFetchItem->ReturnType)
					{
						if (null === $this->_aFetchCallbacks)
						{
							$this->_aFetchCallbacks = array();
						}
						$this->_aFetchCallbacks[str_replace('BODY.PEEK', 'BODY', $oFetchItem->Type)]
							= $oFetchItem->GetCallback();
					}
				}
			}

			while (true)
			{
				if (feof($this->rConnect))
				{
					$this->oFollowers->Write(' break read by feof.',
						CCoreBaseFollowersCollection::ERROR);
					
					break;
				}

				$this->_iResponseBufParsedPos = 0;
				$this->_getNextBuffer();
				
				$oImapResponse = new CCoreNetImapResponse();
				$this->ParseResponseBranch($oImapResponse);
				$aResult[] = $oImapResponse;
				if ($sEndTag === $oImapResponse->Tag)
				{
					break;
				}
				
				unset($oImapResponse);
			}

			$this->_aFetchCallbacks = null;
		}
		$this->_aLastResponse = $aResult;
		return $this->_aLastResponse;
	}

	private function _readLiteralCall($sLiteralAtomUpperCase, $iLiteralLen)
	{
		if (isset($this->_aFetchCallbacks[$sLiteralAtomUpperCase]))
		{
			$sCallBackName = (is_array($this->_aFetchCallbacks[$sLiteralAtomUpperCase])
				&& 2 == count($this->_aFetchCallbacks[$sLiteralAtomUpperCase]))
					? '$object->'.$this->_aFetchCallbacks[$sLiteralAtomUpperCase][1].'()'
					: $this->_aFetchCallbacks[$sLiteralAtomUpperCase].'()';
			
			$this->oFollowers->Write('#INFO: Callback '.$sCallBackName.
				' for '.$sLiteralAtomUpperCase.' - try to read '.$iLiteralLen.' bytes.',
				CCoreBaseFollowersCollection::INFO);

			$rImapLiteralStream = CCoreNetImapStreamWrapper::CreateLiteralStream($this->rConnect, $iLiteralLen);

			$mResult = call_user_func($this->_aFetchCallbacks[$sLiteralAtomUpperCase],
				$rImapLiteralStream, $sLiteralAtomUpperCase);

			$iNotReadLiteralLen = 0;
			while (!feof($rImapLiteralStream))
			{
				$sBuf = fread($rImapLiteralStream, 1024);
				if (false !== $sBuf)
				{
					$iNotReadLiteralLen += strlen($sBuf);
					continue;
				}
				break;
			}

			if ($iNotReadLiteralLen > 0)
			{
				$this->oFollowers->Write('#INFO: not read literal size '.$iNotReadLiteralLen.' bytes.',
					CCoreBaseFollowersCollection::WARNING);
			}

			if (is_resource($rImapLiteralStream))
			{
				fclose($rImapLiteralStream);
			}

			$rImapLiteralStream = null;
			$this->_sResponseBuffer = '';
		}
		else
		{
			$this->_getNextBuffer($iLiteralLen);
			$mResult = $this->_sResponseBuffer;
		}
		
		return $mResult;
	}

	/**
	 * @todo
	 * @param CCoreNetImapResponse $oImapResponse
	 * @param bool $bTreatAsAtom
	 * @return mixed
	 */
	public function ParseResponseBranch(CCoreNetImapResponse $oImapResponse = null, $bTreatAsAtom = false)
	{
		$iPos = $this->_iResponseBufParsedPos;
		$sPreviousAtomUpperCase = null;
		$bIsEndOfList = false;
		$sAtomBuilder = $bTreatAsAtom ? '' : null;
		$bIsClosingBracketSquare = false;
		$bIsGotoDefault = false;

		$aList = array();
		if ($oImapResponse)
		{
			$aList =& $oImapResponse->ResponseList;
		}
		
		while (!$bIsEndOfList)
		{
			$iBufferEndIndex = strlen($this->_sResponseBuffer) - 3;
			if ($iPos > $iBufferEndIndex)
			{
				break;
			}

			$sChar = $this->_sResponseBuffer[$iPos];
			if ($bIsGotoDefault)
			{
				$sChar = 'GOTODEFAULT';
				$bIsGotoDefault = false;
			}
			
			switch ($sChar)
			{
				case ']':
				case ')':
					$sPreviousAtomUpperCase = null;
					$iPos++;
					$bIsEndOfList = true;
					break;
				case ' ':
					if ($bTreatAsAtom)
					{
						$sAtomBuilder .= ' ';
					}
					$iPos++;
					break;
				case '[':
					$bIsClosingBracketSquare = true;
				case '(':
					$sPreviousAtomUpperCase = null;
					if ($bTreatAsAtom)
					{
						$sAtomBuilder .= ($bIsClosingBracketSquare) ? '[' : '(';
					}
					$iPos++;

					$this->_iResponseBufParsedPos = $iPos;
					if ($bTreatAsAtom)
					{
						$sAtomBlock = $this->ParseResponseBranch(null, true);
						$sAtomBuilder .= $sAtomBlock;
						$iPos = $this->_iResponseBufParsedPos;
						$sAtomBuilder .= ($bIsClosingBracketSquare) ? ']' : ')';
					}
					else
					{
						$aSubItems = $this->ParseResponseBranch(null, false);
						$aList[] = $aSubItems;
						$iPos = $this->_iResponseBufParsedPos;
						if (null !== $oImapResponse && $oImapResponse->IsStatusResponse)
						{
							$oImapResponse->OptionalResponse = $aSubItems;
							// Got optional response in status response, the rest of the line is human-readable text.
							$bIsGotoDefault = true;
							break;
						}
					}
					break;
				case '{':
					$bIsLiteralParsed = false;
					$mLiteralEndPos = strpos($this->_sResponseBuffer, '}', $iPos);
					if (false !== $mLiteralEndPos && $mLiteralEndPos > $iPos)
					{
						$sLiteralLenAsString = substr($this->_sResponseBuffer, $iPos + 1, $mLiteralEndPos - $iPos - 1);
						if (is_numeric($sLiteralLenAsString))
						{
							$iLiteralLen = (int) $sLiteralLenAsString;
							$bIsLiteralParsed = true;
							$iPos = $mLiteralEndPos + 3;

							$sLiteral = $this->_readLiteralCall($sPreviousAtomUpperCase, $iLiteralLen);
							if (!$bTreatAsAtom)
							{
								$aList[] = $sLiteral;
							}
							
							$this->_getNextBuffer();
							$iPos = 0;
							break;
						}
					}
					if (!$bIsLiteralParsed)
					{
						// On error parsing literal, skip the problem place.
						$iPos = $iBufferEndIndex;
					}
					$sPreviousAtomUpperCase = null;
					break;
				case '"':
					$bIsQuotedParsed = false;
					while (true)
					{
						$iClosingPos = $iPos + 1;
						if ($iClosingPos >= $iBufferEndIndex)
						{
							// Closing '=' will not fit even if it immediately follows the opening one.
							break;
						}

						$iClosingPos = strpos($this->_sResponseBuffer, '"', $iClosingPos);
						if (false === $iClosingPos)
						{
							break;
						}

						// $iSlashCount will contain the number of back-slashes before closing quote.
						$iSlashCount = 0;
						while ($this->_sResponseBuffer[$iClosingPos - $iSlashCount - 1] === '\\')
						{
							$iSlashCount++;
						}

						// Even number of back-slashes denotes encoded slashes. Odd number denotes
						// zero or more encoded back-slashes and an encoded quote.
						if ($iSlashCount % 2 == 1)
						{
							// That was encoded quote - \", not actually the end of quoted string.
							continue;
						}
						else
						{
							$bIsQuotedParsed = true;
							if ($bTreatAsAtom)
							{
								// Quoted string is copied as-is, including quotes.
								$sAtomBuilder .= substr($this->_sResponseBuffer, $iPos, $iClosingPos - $iPos + 1);
							}
							else
							{
								$aList[] = substr($this->_sResponseBuffer, $iPos + 1, $iClosingPos - $iPos - 1);
							}
							$iPos = $iClosingPos + 1;
							break;
						}
					}
					if (!$bIsQuotedParsed)
					{
						// On error parsing literal, skip the problem place.
						$iPos = $iBufferEndIndex;
					}
					$sPreviousAtomUpperCase = null;
					break;
				case 'GOTODEFAULT':
				default:
					// Deal with atoms and human-readable text here.
					$iCharBlockStartPos = $iPos;

					if (null !== $oImapResponse && $oImapResponse->IsStatusResponse)
					{
						// In case of status response, any atom after first two atoms (and possibly 1 optional response)
						// means human-readable text till the end of line. Special thing about atoms in human-readble
						// text is that they are not atoms actually. They can be zero-length and can contain spaces and
						// other delimiters. So we do some kind of special processing for them. Thus, we declare everything
						// after known atoms and optional response is human-readable text.
						$iPos = $iBufferEndIndex;

						while ($iPos > $iCharBlockStartPos && $this->_sResponseBuffer[$iCharBlockStartPos] == ' ')
						{
							// Remove trailing space in the beginning of the human-readable text.
							$iCharBlockStartPos++;
						}
					}

					$bIsAtomDone = false;
					while (!$bIsAtomDone && ($iPos <= $iBufferEndIndex))
					{
						$sCharDef = $this->_sResponseBuffer[$iPos];
						switch ($sCharDef)
						{
							case '[':
								if (null === $sAtomBuilder)
								{
									// We create StringBuilder only if required. Two cases possible:
									// 1) treatAsString mode (atom is StringBuilder from the very beginning)
									// 2) we got '[' as non-first symbol in response data
									// We could create StringBuilder any time we encounter an atom in response data
									// but it would be the waste of resources (most atoms in response data are simple
									// char blocks with no special symbols and thus do not require concatenation).
									$sAtomBuilder = '';
								}

								// Append BODY[ from BODY[...] response.
								$sAtomBuilder .= substr($this->_sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos + 1);

								$iPos++;
								$this->_iResponseBufParsedPos = $iPos;
								$sListBlock = $this->ParseResponseBranch(null, true);
								if (null !== $sListBlock)
								{
									$sAtomBuilder .= $sListBlock.']';
								}
								$iPos = $this->_iResponseBufParsedPos;
								$iCharBlockStartPos = $iPos;
								break;
							case ' ':
							case ']':
							case ')':
								$bIsAtomDone = true;
								break;
							default:
								$iPos++;
								break;
						}
					}

					// If pos == charBlockStartPos (zero length) and nothing stored in atomBuilder, this means
					// we got no human-readable text in status response. In all other cases, pos > charBlockStartPos
					// because real atoms are always not empty.
					if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder)
					{
						$sLastCharBlock = substr($this->_sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
						if (null === $sAtomBuilder)
						{
							// When atomBuilder is null, this also means !treatAsAtom.
							$aList[] = $sLastCharBlock;
							$sPreviousAtomUpperCase = strtoupper($sLastCharBlock);
						}
						else
						{
							// This case is never top-level case (when response != null). We get here only
							// when parsing internals of BODY[...] literals.
							$sAtomBuilder .= $sLastCharBlock;

							if (!$bTreatAsAtom)
							{
								$aList[] = $sAtomBuilder;
								$sPreviousAtomUpperCase = strtoupper($sAtomBuilder);
								$sAtomBuilder = null;
							}
						}
						if (null !== $oImapResponse)
						{
							if (1 === count($aList))
							{
								$oImapResponse->Tag = $aList[0];
								if ('+' === $oImapResponse->Tag)
								{
									$oImapResponse->ResponseType = ECoreNetImapResponseType::Continuation;
								}
								else if ('*' === $oImapResponse->Tag)
								{
									$oImapResponse->ResponseType = ECoreNetImapResponseType::Untagged;
								}
								else if (0 === strpos($oImapResponse->Tag, self::TAG_PREFIX))
								{
									$oImapResponse->ResponseType = ECoreNetImapResponseType::Tagged;
								}
								else
								{
									$oImapResponse->ResponseType = ECoreNetImapResponseType::Unknown;
								}
							}
							else if (2 === count($aList))
							{
								$oImapResponse->StatusOrIndex = strtoupper($aList[1]);

								if ($oImapResponse->StatusOrIndex == ECoreNetImapResponseStatus::Ok ||
									$oImapResponse->StatusOrIndex == ECoreNetImapResponseStatus::No ||
									$oImapResponse->StatusOrIndex == ECoreNetImapResponseStatus::Bad ||
									$oImapResponse->StatusOrIndex == ECoreNetImapResponseStatus::Bye ||
									$oImapResponse->StatusOrIndex == ECoreNetImapResponseStatus::Preauth)
								{
									$oImapResponse->IsStatusResponse = true;
								}
							}
							else if (ECoreNetImapResponseType::Continuation === $oImapResponse->ResponseType)
							{
								// $oImapResponse->HumanReadable = $sLastCharBlock; #TODO analyze
							}
							else if ($oImapResponse->IsStatusResponse)
							{
								$oImapResponse->HumanReadable = $sLastCharBlock;
							}
						}
					}
					break;
			}
		}
		
		$this->_iResponseBufParsedPos = $iPos;
		return $bTreatAsAtom ? $sAtomBuilder : $aList;
	}

	private function _isSuccess(array $aResult)
	{
		$iCnt = count($aResult);
		return ($iCnt > 0  && $aResult[$iCnt - 1]->IsStatusResponse &&
				ECoreNetImapResponseStatus::Ok === $aResult[$iCnt - 1]->StatusOrIndex);
	}

	private function _isNextSuccess(array $aResult)
	{
		$iCnt = count($aResult);
		return ($iCnt > 0  && ECoreNetImapResponseType::Continuation === $aResult[$iCnt - 1]->ResponseType);
	}

	/**
	 * @param array $naParams = null
	 * @return string
	 */
	private function _prepearParamLine($naParams = null)
	{
		CCore::CheckTypes(array('$naParams'));

		$sReturn = '';
		if (null !== $naParams && count($naParams) > 0)
		{
			foreach ($naParams as $mParamItem)
			{
				if (is_array($mParamItem) && count($mParamItem) > 0)
				{
					$sReturn .= ' ('.trim($this->_prepearParamLine($mParamItem)).')';
				}
				else if (is_string($mParamItem))
				{
					$sReturn .= ' '.$mParamItem;
				}
			}
		}
		return $sReturn;
	}
	
	/**
	 * @return string
	 */
	private function _getNewTag()
	{
		$this->_iTagCount++;
		return $this->_getCurrentTag();
	}

	/**
	 * @return string
	 */
	private function _getCurrentTag()
	{
		return self::TAG_PREFIX.$this->_iTagCount;
	}

	/**
	 * @param string $sLineForEscape
	 * @param bool $bAddQuot = false
	 * @return string
	 */
	private function _escapeString($sLineForEscape, $bAddQuot = false)
	{
		CCore::CheckTypes(array('$sLineForEscape', '$bAddQuot'));

		$sReturn = strtr($sLineForEscape, array('"' => '\\"', '\\' => '\\\\'));
		return ($bAddQuot) ? '"'.$sReturn.'"' : $sReturn;
	}

	/**
	 * @param string $sLineForEscape
	 * @return string
	 */
	public function ImapEscapeString($sLineForEscape)
	{
		return $this->_escapeString($sLineForEscape);
	} 
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapResponse
{
	/**
	 * @var array
	 */
	public $ResponseList;

	/**
	 * @var array
	 */
	public $OptionalResponse;

	/**
	 * @var string
	 */
	public $StatusOrIndex;

	/**
	 * @var string
	 */
	public $HumanReadable;

	/**
	 * @var bool
	 */
	public $IsStatusResponse;

	/**
	 * @var string
	 */
	public $ResponseType;

	/**
	 * @var string
	 */
	public $Tag;
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapFetchResponse
{
	/**
	 * @var CCoreNetImapResponse
	 */
	private $_oImapResponse;
	
	/**
	 * @param CCoreNetImapResponse $oImapResponse
	 * @return CCoreNetImapFetchResponse
	 */
	public function __construct(CCoreNetImapResponse &$oImapResponse)
	{
		$this->_oImapResponse =& $oImapResponse;
	}

	public function &GetFetchValue($sFetchItemName)
	{
		$mReturn = null;
		$bNextIsValue = false;

		if (ECoreNetImapFetch::INDEX === $sFetchItemName)
		{
			$mReturn =& $this->_oImapResponse->ResponseList[1];
		}
		else
		{
			foreach ($this->_oImapResponse->ResponseList[3] as &$mItem)
			{
				if ($bNextIsValue)
				{
					$mReturn =& $mItem;
					break;
				}

				if ($sFetchItemName === $mItem)
				{
					$bNextIsValue = true;
				}
			}
		}
		return $mReturn;
	}

	public static function IsValidFetchImapResponse(CCoreNetImapResponse $oImapResponse)
	{
		if ($oImapResponse && true !== $oImapResponse->IsStatusResponse && '*' === $oImapResponse->Tag
			&& ECoreNetImapResponseType::Untagged === $oImapResponse->ResponseType
			&& 3 < count($oImapResponse->ResponseList) && 'FETCH' === $oImapResponse->ResponseList[2]
			&& is_array($oImapResponse->ResponseList[3]))
		{
			return true;
		}
		return false;
	}
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapFolderInformation
{
	/**
	 * @var string
	 */
	public $FolderName;

	/**
	 * @var bool
	 */
	public $IsWrite;

	/**
	 * @var array
	 */
	public $Flags;

	/**
	 * @var array
	 */
	public $PermanentFlags;

	/**
	 * @var int
	 */
	public $Exists;

	/**
	 * @var int
	 */
	public $Recent;

	/**
	 * @var string
	 */
	public $Uidvalidity;

	/**
	 * @param string $sFolderName
	 * @param bool $bIsWrite
	 * @return CCoreNetImapFolderInformation
	 */
	public function __construct($sFolderName, $bIsWrite)
	{
		$this->FolderName = $sFolderName;
		$this->IsWrite = $bIsWrite;
		$this->Exists = 0;
		$this->Recent = 0;
		$this->Flags = array();
		$this->PermanentFlags = array();
	}
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapFolder
{
	/**
	 * @var array
	 */
	public $Flags;

	/**
	 * @var array
	 */
	public $Delimiter;

	/**
	 * @var string
	 */
	public $FullName;

	/**
	 * @return CCoreNetImapFolder
	 */
	public function __construct()
	{
		$this->Flags = array();
		$this->Delimiter = '/';
		$this->FullName = '';
	}
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapFetchItem
{
	const ReturnTypeString = 0;
	const ReturnTypeCallback = 1;

	/**
	 * @var string
	 */
	public $Type;

	/**
	 * @var string
	 */
	public $ReturnType;

	/**
	 * @var mixed
	 */
	private $_mCallback;
	
	public function __construct($sType)
	{
		$this->Type = $sType;
		$this->ReturnType = self::ReturnTypeString;
	}

	public function SetAsString()
	{
		$this->ReturnType = self::ReturnTypeString;
	}
	
	public function SetAsCallback($mCallback)
	{
		$this->_mCallback = $mCallback;
		$this->ReturnType = self::ReturnTypeCallback;
	}

	public function GetCallback()
	{
		return $this->_mCallback;
	}

	/**
	 * @param array $aItems
	 * @param string $sType
	 * @param mixed $mCallback = null
	 */
	public static function AddToArray(array &$aItems, $sType, $mCallback = null)
	{
		$aItems[$sType] = new self($sType);
		if (null !== $mCallback)
		{
			$aItems[$sType]->SetAsCallback($mCallback);
		}
	}
}

/**
 * @package net
 * @subpackage imap
 */
class CCoreNetImapConfig extends ACoreNetConfigWithLoginAndPassword
{
}
